package kandl.automation.junit;

import org.apache.log4j.Logger;
import org.junit.Before;
import org.junit.runner.Description;
import org.junit.runner.Runner;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.model.InitializationError;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;

import kandl.automation.testsuite.TestCase;
import kandl.automation.testsuite.TestProfile;
import kandl.automation.testsuite.TestSuite;
import static com.google.common.collect.Lists.newArrayList;

public class JUnitAutomationRunnerWrapper extends Runner implements DescriptionMapper {

    private static final Logger logger = Logger.getLogger(JUnitAutomationRunnerWrapper.class);

    private static final Class<AutomationTestSuite> TEST_SUITE_ANNOTATION = AutomationTestSuite.class;

    private static final String CONFIG_PACKAGE = "kandl.automation.spring.config";

    private static ApplicationContext context;

    private AutomationRunner automationRunner;

    private Class<?> clazz;

    private Description rootDescription;

    // Ouch! Cannot see any other way of getting the context through the junit framework.
    public static void setContext(ApplicationContext context) {
        JUnitAutomationRunnerWrapper.context = context;
    }

    public JUnitAutomationRunnerWrapper(Class<?> clazz) throws InitializationError {
        this.clazz = clazz;
        this.rootDescription = Description.createSuiteDescription(clazz);

        List<String> suiteNames = newArrayList();

        for (Field field : getAnnotatedFields(clazz, TEST_SUITE_ANNOTATION)) {
            checkField(field);
            suiteNames.add(field.getAnnotation(TEST_SUITE_ANNOTATION).value());
        }
        ApplicationContext context = JUnitAutomationRunnerWrapper.context;
        if (context == null) {
            context = new AnnotationConfigApplicationContext(CONFIG_PACKAGE);
        }
        this.automationRunner = context.getBean(AutomationRunner.class);
        this.automationRunner.setDescriptionMapper(this);

        for (String name : suiteNames) {
            TestSuite testSuite = automationRunner.loadTestSuite(name);
            buildDescriptionForTestSuite(rootDescription, testSuite);
        }
    }

    @Override
    public Description getDescription() {
        return rootDescription;
    }

    @Override
    public void run(RunNotifier runNotifier) {
        runNotifier.fireTestRunStarted(rootDescription);

        instantiateTestClass();
        
        automationRunner.runAll(runNotifier, rootDescription);
    }

    private void instantiateTestClass() {
        try {
            Object test = clazz.newInstance();
            for (Field field : getAnnotatedFields(clazz, TEST_SUITE_ANNOTATION)) {
                field.setAccessible(true);
                String suiteName = field.getAnnotation(TEST_SUITE_ANNOTATION).value();
                field.set(test, automationRunner.getTestSuite(suiteName));
            }

            for (Method method : clazz.getDeclaredMethods()) {
                if (method.isAnnotationPresent(Before.class)) {
                    method.invoke(test);
                }
            }
        }
        catch (Throwable e) {
            logger.error(e);
            throw new RuntimeException(e);
        }
    }

    private List<Field> getAnnotatedFields(Class<?> clazz, Class<? extends Annotation> annotation) {
        List<Field> fields = new ArrayList<Field>();
        for (Field field : clazz.getDeclaredFields()) {
            if (field.isAnnotationPresent(annotation)) {
                fields.add(field);
            }
        }
        return fields;
    }

    private void checkField(Field field) throws InitializationError {
        if  (!TestSuite.class.isAssignableFrom(field.getType())) {
            String msg = "Automation suite must be of type " + TestSuite.class.getCanonicalName() + ": " + field.getName();
            logger.error(msg);
            throw new InitializationError(msg);
        }
    }

    private void buildDescriptionForTestSuite(Description parent, TestSuite testSuite) {
        Description suiteDescription = Description.createSuiteDescription(testSuite.getName());
        parent.addChild(suiteDescription);

        for (TestProfile profile : testSuite.getProfiles()) {
            Description profileDescription = Description.createSuiteDescription(profile.getId());
            suiteDescription.addChild(profileDescription);
            for (TestCase testCase : testSuite.getTestCases(profile)) {
                Description testCaseDescription =
                    Description.createTestDescription(clazz.getName(),
                        automationRunner.getUniqueIdForTestCase(testCase, profile),
                        profile.getId() + "/" + testCase.getId());
                profileDescription.addChild(testCaseDescription);
            }
        }
    }

    @Override
    public Description getDescription(String id, Description parent) {
        for (Description child : parent.getChildren()) {
            if (child.getDisplayName().equals(id)
                || child.getDisplayName().equals(id + "(" + clazz.getName() + ")")) {
                return child;
            }
        }
        String msg = "Test case description hierarchy incorrect for " + id;
        logger.error(msg);
        throw new RuntimeException(msg);
    }
}
